23.05.20
오늘 한 일
- 알고리즘 문제 풀이
- 포트폴리오 마무리 및 자소서 작성
- 프로그라피 팀 회의
- 익스텐션 개발
- Sentry 적용
- ErrorBoundary 적용
- Sentry 적용
- Nest.js 강의 듣기
이력서 작성
이력서는 피그마에 작성하고, 포트폴리오는 노션에 작성했다.
이력서에 조금 더 나에 대한 설명을 추가하고, 포트폴리오에는 프로젝트에 대한 설명을 추가해서 내일까지 마무리해야겠다.
익스텐션 개발
Toast 컴포넌트 구현
Sentry에서 제공하는 ErrorBoundary를 이용하여 에러가 발생하면 토스트를 띄우는 컴포넌트를 구현했다.
// src/components/uis/Toast/index.tsx
type Props = {
message: string;
type: 'success' | 'error';
delay?: number;
};
const Toast = ({ message, type, delay = 3000 }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const toastColor = {
success: 'bg-green-500',
error: 'bg-red-500',
};
useEffect(() => {
setIsOpen(true);
const timer = setTimeout(() => setIsOpen(false), delay);
return () => clearTimeout(timer);
}, []);
return (
<Modal.Background isOpen={isOpen} className='fixed left-0 top-0 z-[1999]'>
<Modal
className={`round-[7px] border-1 fixed left-1/2 top-[20px] flex h-[50px] max-w-[200px] translate-x-[-50%] items-center justify-center rounded-[15px] p-[15px] text-white ${toastColor[type]}`}
>
<p>{message}</p>
</Modal>
</Modal.Background>
);
};
// src/pages/content/App.tsx
<Portal elementId='modal'>
<ErrorBoundary fallback={<Toast message='에러가 발생했습니다.' type='error' />}>
<ContentModal ref={modalRef} onClick={handleModalClick} isOpen={isModalOpen} />
</ErrorBoundary>
</Portal>
ErrorBoundary에서 비동기 에러를 잡지 못하는 문제
왜 못잡을까?
비동기 함수에서 발생한 에러는 일반적인 라이프 사이클 외부에서 발생한 에러고 렌더링 중 발생하지 않기 때문에 ErrorBoundary에서 잡을 수 없다.
공식문서 Error boundaries do not catch errors for:
- Event handlers (learn more)
- Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
- Server side rendering
- Errors thrown in the error boundary itself (rather than its children)
그래서 try/catch나 catch를 이용하여 에러를 잡아야 한다.
나는 try/catch 문을 사용하지 않고 catch를 이용했다. async/await를 사용하면 내부 코드들이 동기식으로 읽을 수 있는 장점이 있는데, try/catch를 사용하게 되면 코드가 동기식으로 읽히지 않아 async/await의 장점을 깨뜨리는 것 같았다. 그래서 catch를 이용하여 에러를 잡았다.
useError
훅을 만들어서 catch에서 setError를 호출하고, error state가 존재하면 에러를 던져주어 ErrorBoundary에서 잡을 수 있도록 했다.
// src/hooks/useError.ts
const useError = () => {
const [error, setError] = useState<Error | null>(null);
const catchAsyncError = (error: Error) => {
setError(error);
};
return { error, catchAsyncError };
};
예외 처리할 때 throw하지 않고 return을 해주는 경우에도 로그를 남기고 싶다면?
이런 경우에는 Sentry의 captureException
를 사용하면 된다.
크롤링 중에 받아와야할 것을 받아오지 못했을 때, 에러를 던지지 않고 return을 해주었는데, 이런 경우에도 로그를 남기고 싶었다. 그래서 catchAsyncError
에서 captureException
를 호출해주었다.
const getVideoAtCourseDocument = ($: cheerio.CheerioAPI, courseId: string) => {
return $('.total_sections .activity.vod .activityinstance')
.map((i, el) => {
const link = $(el).find('a').attr('href');
if (!link) {
captureException(new Error(`동영상 링크가 없습니다. courseId: ${courseId}`)); // here
return;
}
const id = getLinkId(link);
const title = $(el).find('.instancename').clone().children().remove().end().text().trim();
const [, endAt, timeInfo] = $(el)
.find('.displayoptions')
.text()
.split(/ ~ |,/)
.map((str) => str.trim());
const v: Video = {
type: 'video',
hasSubmitted: false,
id,
courseId,
title,
endAt,
timeInfo,
};
return v;
})
.get();
};
리팩토링
ContentModal 컴포넌트에서 너무 많은 역할을 하고 있어서 분리하는 작업을 했다.
useScrollLock hook
일단 Modal이 켜질 때 뒤에 있는 컨텐츠들이 스크롤되지 않도록 useEffect
를 통해 스타일을 바꿔주어 구현했었는데, 이 부분을 훅으로 빼주었다.
const useScrollLock = () => {
const scrollLock = () => {
document.body.style.cssText = `
position: fixed;
top: -${window.scrollY}px;
overflow-y: scroll;
width: 100%;`;
};
const scrollUnlock = () => {
const scrollY = document.body.style.top;
document.body.style.cssText = '';
window.scrollTo(0, parseInt(scrollY || '0', 10) * -1);
};
return { scrollLock, scrollUnlock };
};
export default useScrollLock;
useFetchData hook
컴포넌트에서 데이터를 받아오는 로직을 분리하여 훅으로 만들었다.
// src/hooks/useFetchData.ts
type dataType = {
courseList: Course[];
activityList: ActivityType[];
updateAt: number;
};
const useFetchData = () => {
const [pos, setPos] = useState(0);
const [data, setData] = useState<dataType>({
courseList: [{ id: '-1', title: '전체' }],
activityList: [],
updateAt: 0,
});
const getData = async () => {
const courses = await getCourses();
const activities = await allProgress(
courses.map((course) => getActivities(course.id)),
(progress) => setPos(progress)
).then((activities) => activities.flat());
const updateAt = new Date().getTime();
setData({
courseList: [{ id: '-1', title: '전체' }, ...courses],
activityList: activities,
updateAt,
});
setPos(0);
chrome.storage.local.set({
courses,
activities,
updateAt,
});
};
const getLocalData = () => {
chrome.storage.local.get(({ updateAt, courses, activities }) => {
setData({
courseList: [{ id: '-1', title: '전체' }, ...courses],
activityList: activities,
updateAt,
});
});
};
return { getData, getLocalData, data, pos };
};
export default useFetchData;
모달을 열었을 때 업데이트 된 시간이 10분이 지났으면 getData
, 아니면 getLocalData
를 호출하도록 했다.
// src/components/ContentModal.tsx
const ContentModal = ({ isOpen, onClick }: Props, ref: React.Ref<HTMLDivElement>) => {
const [selectedCourse, setSelectedCourse] = useState<Course>({ id: '-1', title: '전체' });
const [statusType, setStatusType] = useState<{ id: number; title: string }>(status[0]);
const [isRefresh, setIsRefresh] = useState(false);
const { catchAsyncError } = useError();
const { scrollLock, scrollUnlock } = useScrollLock();
const [getData, getLocalData, data, pos] = useFetchData();
const { courseList, activityList, updateAt } = data;
useEffect(() => {
if (!isRefresh) return;
getData()
.then(() => setIsRefresh(false))
.catch(error => catchAsyncError(error));
}, [isRefresh]);
useEffect(() => {
if (!isOpen) return;
scrollLock();
if (!isRefresh)
chrome.storage.local.get(['updateAt'], ({ updateAt }) => {
if (!updateAt) return setIsRefresh(true);
const diff = new Date().getTime() - updateAt;
const isOverRefreshTime = diff > REFRESH_TIME;
if (!isOverRefreshTime) {
getLocalData();
} else {
setIsRefresh(true);
}
});
return scrollUnlock;
}, [isOpen]);
return (
//...
)
}
아직 useEffect를 사용하는 부분이 많아서 리팩토링이 필요하다.
Refresh할 때 로직을 분리해야할 것 같다.
내일 할 일
- 알고리즘 문제 풀이
- 포트폴리오 다듬고 자기소개서 작성 후 제출
- IT 특강 팀플 회의
- 익스텐션 에타에 홍보?